package org.loong.crypto.core.algorithm;

import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.gm.GMObjectIdentifiers;
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.loong.crypto.core.Algorithm;
import org.loong.crypto.core.AlgorithmFamily;

/**
 * The Cipher (Encryption) algorithm.
 */
public final class CipherAlgorithm extends Algorithm {

    /**
     * Encryption using AES algorithm.
     */
    public static final CipherAlgorithm AES = new CipherAlgorithm("AES", NISTObjectIdentifiers.aes);

    /**
     * Encryption using AESWrap algorithm.
     */
    public static final CipherAlgorithm AES_WRAP = new CipherAlgorithm("AESWrap");

    /**
     * Encryption using ARCFOUR algorithm.
     */
    public static final CipherAlgorithm ARCFOUR = new CipherAlgorithm("ARCFOUR");

    /**
     * Encryption using Blowfish algorithm.
     */
    public static final CipherAlgorithm BLOWFISH = new CipherAlgorithm("Blowfish");

    /**
     * Encryption using CCM algorithm.
     */
    public static final CipherAlgorithm CCM = new CipherAlgorithm("CCM");

    /**
     * Encryption using DES algorithm.
     */
    public static final CipherAlgorithm DES = new CipherAlgorithm("DES");

    /**
     * Encryption using DESede algorithm.
     */
    public static final CipherAlgorithm DESEDE = new CipherAlgorithm("DESede");

    /**
     * Encryption using DESedeWrap algorithm.
     */
    public static final CipherAlgorithm DESEDE_WRAP = new CipherAlgorithm("DESedeWrap");

    /**
     * Encryption using ECIES algorithm.
     */
    public static final CipherAlgorithm ECIES = new CipherAlgorithm("ECIES");

    /**
     * Encryption using GCM algorithm.
     */
    public static final CipherAlgorithm GCM = new CipherAlgorithm("GCM");

    /**
     * Encryption using RC2 algorithm.
     */
    public static final CipherAlgorithm RC2 = new CipherAlgorithm("RC2");

    /**
     * Encryption using RC4 algorithm.
     */
    public static final CipherAlgorithm RC4 = new CipherAlgorithm("RC4");

    /**
     * Encryption using RC5 algorithm.
     */
    public static final CipherAlgorithm RC5 = new CipherAlgorithm("RC5");

    /**
     * Encryption using RSA algorithm.
     */
    public static final CipherAlgorithm RSA = new CipherAlgorithm("RSA", PKCSObjectIdentifiers.rsaEncryption);

    /**
     * Encryption using SM2 algorithm.
     */
    public static final CipherAlgorithm SM2 = new CipherAlgorithm("SM2", GMObjectIdentifiers.sm2p256v1);

    /**
     * Encryption using SM4 algorithm.
     */
    public static final CipherAlgorithm SM4 = new CipherAlgorithm("SM4", GMObjectIdentifiers.sm_scheme.branch("104"));

    /**
     * The supported encryption algorithms.
     */
    public static final Map<String, CipherAlgorithm> SUPPORTED_ALGORITHMS;

    static {
        Map<String, CipherAlgorithm> algs = new LinkedHashMap<>();
        algs.put(AES.getName(), AES);
        algs.put(AES_WRAP.getName(), AES_WRAP);
        algs.put(ARCFOUR.getName(), ARCFOUR);
        algs.put(BLOWFISH.getName(), BLOWFISH);
        algs.put(CCM.getName(), CCM);
        algs.put(DES.getName(), DES);
        algs.put(DESEDE.getName(), DESEDE);
        algs.put(DESEDE_WRAP.getName(), DESEDE_WRAP);
        algs.put(ECIES.getName(), ECIES);
        algs.put(GCM.getName(), GCM);
        algs.put(RC2.getName(), RC2);
        algs.put(RC4.getName(), RC4);
        algs.put(RC5.getName(), RC5);
        algs.put(RSA.getName(), RSA);
        algs.put(SM2.getName(), SM2);
        algs.put(SM4.getName(), SM4);
        SUPPORTED_ALGORITHMS = Collections.unmodifiableMap(algs);
    }

    /**
     * Encryption algorithm family.
     */
    public static final class Family extends AlgorithmFamily<CipherAlgorithm> {

        private static final long serialVersionUID = 1L;

        /**
         * Super family of all symmetric (shared key based) encryption algorithms.
         */
        public static final Family SYMMETRIC = new Family(AES, AES_WRAP, ARCFOUR, BLOWFISH, CCM, DES, DESEDE, DESEDE_WRAP, RC2, RC4, RC5, SM4);

        /**
         * Super family of all asymmetric (public / private key based) encryption algorithms.
         */
        public static final Family ASYMMETRIC = new Family(ECIES, RSA, SM2);

        /**
         * Creates a new encryption algorithm family.
         *
         * @param algs The encryption algorithms of the family. Must not be {@code null}.
         */
        public Family(final CipherAlgorithm... algs) {
            super(algs);
        }
    }

    /**
     * Creates a new encryption algorithm name.
     * 
     * @param name the algorithm name. Must not be {@code null}.
     */
    public CipherAlgorithm(final String name) {
        this(name, null);
    }

    /**
     * Creates a new encryption algorithm name.
     * 
     * @param name the algorithm name. Must not be {@code null}.
     * @param oid the algorithm object identifier.
     */
    public CipherAlgorithm(final String name, final ASN1ObjectIdentifier oid) {
        super(name, oid);
    }

    /**
     * Parses a encryption algorithm from the specified string.
     *
     * @param name the algorithm name. Must not be {@code null}.
     * @return the encryption algorithm.
     * @throws NoSuchAlgorithmException the ${@link NoSuchAlgorithmException}
     */
    public static CipherAlgorithm find(final String name) throws NoSuchAlgorithmException {
        CipherAlgorithm algorithm = SUPPORTED_ALGORITHMS.get(name);
        if (algorithm == null) {
            throw new NoSuchAlgorithmException("Unknown encryption algorithm type requested: " + name);
        }

        return algorithm;
    }

    /**
     * Find a cipher algorithm from the specified string.
     *
     * @param algorithmId the cipher algorithm id. Must not be {@code null}.
     * @return the cipher algorithm.
     * @throws NoSuchAlgorithmException the ${@link NoSuchAlgorithmException}
     */
    public static CipherAlgorithm findByAlgorithmId(final AlgorithmIdentifier algorithmId) throws NoSuchAlgorithmException {
        final ASN1ObjectIdentifier oid = algorithmId.getAlgorithm();
        return SUPPORTED_ALGORITHMS.values().stream().filter(algorithm -> Objects.nonNull(algorithm.getOID()) && Objects.equals(algorithm.getOID(), oid)).findFirst()
                .orElseThrow(() -> new NoSuchAlgorithmException("Unknown cipher algorithm type requested: " + oid));
    }
}
